package uno.net;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class UnoThreadFactory {
	private static final ReentrantReadWriteLock areYouAliveLock = new ReentrantReadWriteLock();
	private static final Lock areYouAliveLock_write = areYouAliveLock.writeLock();
	private static final ReentrantReadWriteLock updateLeadershipStatusLock = new ReentrantReadWriteLock();
	private static final Lock updateLeadershipStatusLock_write = updateLeadershipStatusLock.writeLock();
	private Object peer;
	private void setLocalPeer(Object givenPeer) { this.peer = givenPeer; }
	private Object getLocalPeer() { return this.peer; }

	public UnoThreadFactory(Object peerIstance) {
		this.setLocalPeer(peerIstance);
	}

	/////////////////////////////////////////
	// Client Threads
	/////////////////////////////////////////

	/**
	 * prettamente utilizzato dal primo leader del gioco
	 * controlla che la lista dei giocatori sia arrivata per iniziare a giocare
	 */
	class StartToPlayThread extends Thread {
		private UnoPlayer localPeer;
		public StartToPlayThread() { this.localPeer = (UnoPlayer)getLocalPeer(); }
		public void run() {
			try {
				while(true) {
					// attende che arrivi la lista dei giocatori
					Thread.sleep(1000);
					// controlla se è arrivata la lista
					if(localPeer.getCanIStartToPlay()) {
						// se è arrivata comincio a giocare
						localPeer.startsToPlay();
						break;
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}


	/**
	 * controlla che un determinato client sia ancora attivamente in gioco 
	 */ 
	class AreYouAliveThread extends Thread {
		private boolean killed;
		public void kill() { killed = true; }

		// remotePeer è il player sul quale effettuare l'invocazione remota
		private UnoPlayerRM remotePeer;
		private UnoPlayerID leaderID;
		public AreYouAliveThread(UnoPlayerID leaderID) {
			this.killed = false;
			this.remotePeer = UnoPlayer.playerIDToPlayer(leaderID);
			this.leaderID = leaderID;
		}

		/**
		 * Gestisce i guasti del leader
		 * */
		private void leaderCrashesTollerance(UnoPlayer localPeer) {
			System.out.println("* leaderCrashesTollerance: lock? " + getName());
			updateLeadershipStatusLock_write.lock();
			System.out.println("* leaderCrashesTollerance: locked. " + getName());
			try {
			// nel caso il leader si sia guastato...
			// - aggiorno la lista dei giocatori
			// - aggiorna anche la relativa vista grafica
			// quindi...		
			// mi salvo l'id del vecchio leader
			System.err.println("*[INFO] il leader " + leaderID + " ha avuto un guasto, elezione?");
			// rimuovo il vecchio leader dalla lista e calcolo la nuova leadership
			localPeer.removePlayerFromList(leaderID, true);				
			// gestisco i guasti relativi alla distribuzione iniziale delle carte quindi...
			// controllo di essere il nuovo leader
			if(localPeer.amICurrentLeader()) {
				// se lo sono controllo mazzo e mano
				if(localPeer.getDeck() == null || localPeer.getHandOfCard() == null) {
					// se non ho ricevuto nulla significa che il giro di distribuzione non è terminato quindi...
					// rispedisco tutto da capo
					System.err.println("*[INFO] consegna carte incompleta");
					// setto il leader corrente a null in modo tale da far funzionare
					// startsToPlay(che richiama checkAndChangeLeadership
					 localPeer.setCurrentLeaderID(null);
					 localPeer.startsToPlay();	
				} else if(localPeer.getCardsDistribuitionThread().getReadyTokenState() == ReadyTokenState.NOT_SENT) {
					// se invece il mazzo e la mano mi sono arrivati ma il token no, spedisco solo un token ready
					System.err.println("*[INFO] il leader ha avuto un guasto prima che il token ready completasse il giro dell'anello");
					System.out.println("*[LOG] tutti i peer hanno ricevuto le carte, inizia il gioco...");
					localPeer.sendReadyToken();
				} 
			}
			} finally {
				System.out.println("* leaderCrashesTollerance: release. " + getName());
				updateLeadershipStatusLock_write.unlock();
				System.out.println("* leaderCrashesTollerance: released. " + getName());
				
			}
		}
		
		public void run() {
			System.out.println("* areYouAlive: lock? " + getName());
			areYouAliveLock_write.lock();
			System.out.println("* areYouAlive: locked. " + getName());
			try{
				UnoPlayer localPeer = (UnoPlayer)getLocalPeer();
				try {
					if (remotePeer == null) { throw new RemoteException(); }
					// ciclo finché non è arrivato l'aggiornamento dello stato
					while(!(this.killed)) {
						// aspetto che il leader faccia la sua mossa
						Thread.sleep(2000);
						remotePeer.areYouAlive();
						System.out.println("* rispondi?");
					}				
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (RemoteException e) {
					// gestione dei guasti del leader
					leaderCrashesTollerance(localPeer);
				}
			}finally{
				System.out.println("* areYopuAlive: release. " + getName());
				areYouAliveLock_write.unlock();
				System.out.println("* areYopuAlive: released. " + getName());
			}
		}		
	}

	/**
	 * aggiorna lo stato locale del gioco
	 * @author francesco
	 */
	class UpdateGameStatusThread extends Thread {
		private UnoPlay play;
		private UnoPlayerID remotePeerId;
		private UnoPlayerRM remotePeer;
		private UnoPlayer localPeer;

		public UpdateGameStatusThread(UnoPlayerID remotePeerID, UnoPlay givenP) {
			this.play = givenP;
			this.remotePeerId = remotePeerID;
			this.remotePeer = UnoPlayer.playerIDToPlayer(remotePeerID);
			this.localPeer = (UnoPlayer)getLocalPeer();
		}

		public void run() {
			System.out.println("* updateLeadershipStatus: lock? " + getName());
			updateLeadershipStatusLock_write.lock();
			System.out.println("* updateLeadershipStatus: locked. " + getName());
			try {
				boolean amICurrentLeader = localPeer.amICurrentLeader();
				// invio lo stato del gioco al peer successivo
				if(remotePeer != null) {
					remotePeer.updateGameStatus(play);
					System.out.println("*[LOG] stato del gioco propagato a " + remotePeerId.toString());
				} else throw new RemoteException();
				
				// AGGIORNAMENTO STATO DELLA LEADERSHIP
				// una volta propagato lo stato, se non sono il leader corrente e propago il nuovo stato
				// per la prima volta...
				// aggiorno anche lo stato della leadership e lancio AreYouAlive sul nuovo leader
				// mentre il leader calcola la nuova leadership solo al termine del token ring
				
				// FIXME
				// RICHIESTA GESTIONE DI CONCORRENZA CON LEADER CRASH TOLLERANCE
				if((!(amICurrentLeader)) && (play.getID() == localPeer.getGameStatus().getID()) && (UnoPlayer.idEquals(play.getOwnerId(), localPeer.getCurrentLeaderID()))) {
					localPeer.updateLeadershipStatus();
					if(localPeer.amICurrentLeader()) localPeer.setEnabledGUI(true);
				}
			} catch(RemoteException e) {
				// nel caso che la propagazione al successivo non sia andata a buon fine...
				// - elimino il player che non risponde, quindi che ha subito un guasto
				// - aggiorno la lista dei giocatori
				// - aggiorna anche la relativa vista grafica
				System.out.println("*[LOG] rilevato guasto del peer successivo...");
				this.localPeer.removePlayerFromList(this.remotePeerId, false);
				System.out.println("*[LOG] giocatore eliminato dalla lista");
				// - ripropago lo stato al successivo
				System.out.println("*[LOG] ri-propagazione dello stato al successivo peer in corso...");
				this.localPeer.propagateStatus(play);
			} catch(Exception e) {
				e.printStackTrace();
			} finally {
				System.out.println("* updateLeadershipStatus: release. " + getName());
				updateLeadershipStatusLock_write.unlock();
				System.out.println("* updateLeadershipStatus: released. " + getName());
				
			}
		}
	}
	
	
	class UpdateGameStatusRepeaterThread extends Thread {
		private UnoPlayer localPeer;
		private UnoPlay play;
		private boolean killed;
		public void kill() { this.killed = true;}
		
		public UpdateGameStatusRepeaterThread(UnoPlay play) {
			this.localPeer = (UnoPlayer)getLocalPeer();
			this.play = play;
			this.killed = false;
		}
		
		public void run() {
			try {
				while(!(this.killed)) {
					// effettuo la propagazione...
					this.localPeer.propagateStatus(this.play);
					// aspetto un TIMEOUT...
					Thread.sleep(2000);
					// al termine del timeout...
					// si ripete la propazione dello stato verso il prossimo peer
				}
			} catch (InterruptedException e) { e.printStackTrace(); }	
		}
	}
	
	
	//-----------------------------------------------------------------------------------
	/** gli stati che puo' assumere il token ready */ 
	public static enum ReadyTokenState { NOT_SENT, SENT, PREPARE, RING_FINISHED }
	//-----------------------------------------------------------------------------------
	
	/**
	 * prettamente utilizzato dal primo leader del gioco
	 * controlla che la lista dei giocatori sia arrivata per iniziare a giocare
	 */
	class ReadyToPlayThread extends Thread {
		private UnoPlayer localPeer;
		private UnoPlayerID remotePeerID;
		/** conta quante volte ho rispedito il token */
		private int times;
		/** mantiene in memoria se sono arrivati dei token , serve per evitare ri-invi multipli dei token*/
		//private ArrayList<Object> tokenBuffer;
		
		public ReadyTokenState readyTokenState;
		public ReadyTokenState getReadyTokenState() { return readyTokenState; }
		public synchronized void  setReadyTokenStatus(ReadyTokenState readyTokenState) { this.readyTokenState = readyTokenState; /*System.out.println("cambio lo stato del token ready" + readyTokenState);*/ }

		
		public ReadyToPlayThread() { 
			this.localPeer = (UnoPlayer)getLocalPeer(); 
			this.remotePeerID = null;
			this.readyTokenState = UnoThreadFactory.ReadyTokenState.NOT_SENT;
			this.times = 0;
			//this.tokenBuffer = new ArrayList<Object>();
			//System.out.println("RICHIAMATO IL COSTRUTTORE DI READY TO PLAY THREAD");
		}
		
		
		/**
		 * Spedisce il Token ready al primo peer vivo che trova scorrendo la lista a partire da localPeer (con l'ordinamento dato
		 * da getNextPeerID)
		 * */
		private synchronized void sendTokenToFirstAvailablePlayer() {
			boolean flag = false;
			while(!flag){
				// spedisco il token al peer successivo (inversamente) nella lista
				try {					
					// controllo di non essere rimasto solo nella lista
					if(localPeer.getPlayersList().size() > 1) {
						remotePeerID = localPeer.getNextPeerID();
						UnoPlayerRM remotePeer = UnoPlayer.playerIDToPlayer(remotePeerID);
						if(remotePeer == null) throw new RemoteException();
						remotePeer.sendReadyToken();
						// se arrivo a settare true significa che sono riuscito a spedire il token a qualcuno
						// che non sia io
						flag = true;
					} else {
						// se sono rimasto solo interrompo il ciclo (il flag e' rimasto false)
						break;
					}
				} catch (RemoteException e) {
					System.err.println("*[INFO] il peer successivo ha avuto un guasto, inoltro il token al peer ancora successivo");
					// il peer successivo e' crashato quindi va rimosso dalla lista
					localPeer.removePlayerFromList(remotePeerID, false);
					System.err.println("*[INFO] La lista dei giocatori che credo vivi e': ");
					System.err.println(localPeer.getPlayersList());				
				} catch (Exception e) {
					e.printStackTrace();
				}
			}	
			// se arrivo a questa linea di codice vuol dire che sono uscito dal while e quindi ho trovato qualcuno
			// a cui spedire il token
			if(flag) {
				times++;
				if(times == 1)
					setReadyTokenStatus(ReadyTokenState.SENT);
			} else {
				System.out.println("*[LOG] [leader] Inutile tentare di spedire il token, sono rimasto l'unico giocatore");
			}
		}
		
		/**
		 * Gestisce l'arrivo dei token ready. Decide le decisioni da intraprendere.
		 * */
		public synchronized boolean manageTokens(){ 
			
			boolean flag = true;
			// controllo lo stato del token
			switch(getReadyTokenState()) {
			
			case NOT_SENT:			
				// sono nel caso in cui non ho mai spedito/inoltrato il token
				System.out.println("*[LOG] sono nello stato NOT_SENT, inizio la procedura di spedizione del token ready");
				// setto che ho iniziato la preparazione per il primo invio/inoltro del token
				setReadyTokenStatus(UnoThreadFactory.ReadyTokenState.PREPARE);
				// poi lo inoltro (setto a false in modo che il player inoltre il token)
				flag = false;
				// se non sono il leader visualizzo le carte
				if(!localPeer.amICurrentLeader()) {
					localPeer.showAllInGUI();
					System.out.println("*[LOG] inoltrato il token ready. Ora visualizzo le carte");
				}
				break;

			case SENT: 
				// sono nel caso in cui ho gia' spedito/inoltrato il token almeno una volta 
				// se non sono il leader
				if(!localPeer.amICurrentLeader()) {
					System.out.println("*[LOG] Non sono il leader e sto ri-inoltrando il token");
					// ri-inoltro il token e amen, qualche idiota deve essere morto oppure le comunicazioni sono lente
					flag = false;
				} else { 
					// altrimenti se sono il leader..
					// e non ho gia' visualizzato le carte / mazzo
					if(localPeer.getDeck().isHidden()){
						// setto che il token ha fatto il giro (questo terminera' anche il thread di controllo)
						setReadyTokenStatus(UnoThreadFactory.ReadyTokenState.RING_FINISHED);
						System.out.println("*[LOG] il token ready ha completato il giro dell'anello");
						// e visualizzo le carte facendo iniziare il gioco
						localPeer.showAllInGUI();
						// abilito l'interfaccia
						localPeer.setEnabledGUI(true);
					} else { // se invece ho gia' visualizzato le carte e il mazzo 
						// (puo' capitare quando si verificano dei crash nella distribuzione)
						setReadyTokenStatus(UnoThreadFactory.ReadyTokenState.RING_FINISHED);
						System.out.println("*[LOG] un token ready ha completato il giro dell'anello\nMa non influisce sul gioco in quanto gia' in atto correttamente");
					}
				}
				break;

			case PREPARE: 
				// e' il caso in cui il token mi e' gia' arrivato, ma non ho ancora finito di inoltrarlo.
				// non credo sia utile inoltrare due token.. intaserebbe solo la rete. Quindi non faccio nulla
				System.out.println("*[LOG] Mi e' arrivato un token mentre ero nello stato PREPARE");
				
				break;

			case RING_FINISHED: 
				// se finisco qua significa che mi e' tornato un token dopo che me ne era gia' tornato uno. 
				// quindi lo devo ignorare.
				System.out.println("*[LOG] Mi e' arrivato un token mentre sono nello stato RING_FINISHED. Lo ignoro.");			
			}
			/* APPUNTI
			 Non importa aspettare che il token faccia il giro per far visualizzare le carte
			 perche' se sta passando il token significa che tutti hanno gia' ricevuto le carte
			 quindi le posso visualizzare senza timore di incosistenza. Il leader ad ogni modo
			 aspetta.*/
			return flag;

		}
		
		
		public void run() {
			// Spedisco il token al primo peer vivo che trovo seguendo la lista dopo di me (con l'ordinamento dato
			// da getNextPeerID
			sendTokenToFirstAvailablePlayer();
			
			// ora che il token e' stato spedito, se non sono il leader termino il thread mentre..			
			// se sono il leader
			if(localPeer.amICurrentLeader()) {
				while(true) {		
					try {
						Thread.sleep(1000);
					} catch (InterruptedException ie) {	System.err.println("*[WARNING] InterruptedException: verificatasi durante l'attesa del token ready"); }

					// controlla se è tornato il token, se e' arrivato:
					if(getReadyTokenState() == ReadyTokenState.RING_FINISHED) {
						System.out.println("*[LOG] [leader] termino il processo di ripetizione del token ready...");
						break;
					} else { 
						// altrimenti sparo un altro token al peer successivo
						System.out.println("*[LOG] [leader] il timeout è scaduto, ri-spedisco il token ready...");
						sendTokenToFirstAvailablePlayer();
					}
				}
			}
		}
	}

	/////////////////////////////////////////
	// Server Threads
	/////////////////////////////////////////

	/**
	 * attende che i giocatori iscritti siano almeno il numero minimo
	 */
	class WaitThread extends Thread {
		UnoServer localPeer;
		public WaitThread() { localPeer = (UnoServer)getLocalPeer(); }
		public void run() {
			try {
				while(true) {
					// attende che tutti completino le iscrizioni
					Thread.sleep(1000);
					// controllo se ho raggiunto la quota di giocatori minimi per giocare
					if((localPeer.getPlayersList() != null) && (localPeer.getPlayersList().size() == localPeer.getMinNumOfPlayers())) {
						localPeer.syncPlayers();
						break;
					}
				}
				// il server di iscrizione termina.
				System.out.println("*[LOG] chiusura del server di iscrizione.");
				Naming.unbind(localPeer.getRmiName());
			} catch (RemoteException e) {
				e.printStackTrace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (MalformedURLException e) {
				e.printStackTrace();
			} catch (NotBoundException e) {
				e.printStackTrace();
			}
		}
	}

	/////////////////////////////////////////
	// Thread Factory Methods
	/////////////////////////////////////////

	public StartToPlayThread createStartToPlayThread() {
		return new StartToPlayThread();	
	}

	private AreYouAliveThread areYouAliveThread;
	public AreYouAliveThread getAreYouAliveThread() { return areYouAliveThread; }
	public AreYouAliveThread createAreYouAliveThread(UnoPlayerID peerID) {
		return areYouAliveThread = new AreYouAliveThread(peerID);
	}

	public WaitThread createWaitThread() {
		return new WaitThread();				
	}

	public UpdateGameStatusThread createUpdateGameStatusThread(UnoPlayerID nextPeerID, UnoPlay play) {
		return new UpdateGameStatusThread(nextPeerID, play);				
	}
	
	private UpdateGameStatusRepeaterThread updateGameStatusRepeaterThread;
	public UpdateGameStatusRepeaterThread getUpdateGameStatusRepeaterThread() { return updateGameStatusRepeaterThread; }
	public UpdateGameStatusRepeaterThread createUpdateGameStatusRepeaterThread(UnoPlay play) {
		return updateGameStatusRepeaterThread = new UpdateGameStatusRepeaterThread(play);
	}

	public ReadyToPlayThread createReadyTokenThread() {
		return new ReadyToPlayThread();				
	}
}
